Объектно-реляционное связывание (Object-relational mapping, ORM) реализует привязку объектов Java к БД, позволяя описывать какие таблицы и столбцы БД будут использованы для хранения постоянного состояния объектов.
Системы объектно-реляционного связывания (Object/relational tools) позволяют разработчику сконцентироваться на работе с объектами Java. Они обеспечивают автоматическое связывание объектов с БД, включая автоматическую генерацию кода SQL для выборки и модификации данных в БД. При использовании таких средств нет необходимости написании кода для прямого взаимодействия с БД. Но привязку ваших объектов Java к БД все таки описать нужно.
OpenXava использует Java Persistence API (JPA) для реализации ORM. В данном главе мы расскажем о базовых техниках применения JPA и рассмотрим несколько распространнёных случаев, вызывающих затруднение у начинающих работать с JPA. Дополнительную информацию о JPA можно почерпнуть из документации по Hibernate Annotations (реализация JPA, используемая OpenXava по-умолчанию), также вы можете использовать любую другую документацию по JPA.
Связывание объекта (Entity mapping)
Аннотация @Table (Таблица) указывает основную таблицу для связываемого класса сущности (entity). С помощью аннонтации @SecondaryTable (если речь об одной дополнительной таблице) или @SecondaryTables (если нужно указать несколько таблиц) можно указать дополнительные таблицы.
Пример определения сущности:
Аттрибут name задает имя таблицы, если оно не соответствует соглашению по наименованию JPA.
Аттрибут schema задает схему, в которой будет размещена таблица.
Аттрибуты не обязательны, полный список аттрибутов смотрите в документации JPA.
Если для сущности не указано определение аннонтации @Table, предполагается, что имя таблицы соответствует соглашениям по наименованию, принятым в JPA.
Связывание свойства (Property mapping)
Аннотация @Column (Столбец) используется для указания столбца таблицы, с которым связывается переменная-член класса сущности. Если для свойства аннотация @Column не определена, то при связывании используются значения и соглашения, принятые в JPA по-умолчанию.
Простейший пример описания свойства:
В примере переопределяется имя столбца аттрибутом name и задается длина данных аттрибутом length.
Пример с аннотированием метода установки значения свойства (getter):
Аннотация @JoinColumn (Стоблец, используемый для соединения) используется для определения свойства, которое ссылается на другую сущность.
ПЕР: В примере ниже наша сущность имеет ссылку на Invoice (Счет).
Пример:
Для связывания с композитным внешним ключом (composite foreign keys) используйте @JoinColumns. Это аннотация позволяет группировать вместе аннотации @JoinColumn, относящиеся к одной ссылке.
При использовании @JoinColumns аттрибуты name и referencedColumnName должны быть определены для каждой аннотации @JoinColumn.
Пример:
При использовании @OneToMany для коллекций обычно нет необходимости делать что-либо дополнительное на второй стороне ассоциации. Но если вы используете @ManyToMany, то возможно будет полезным использовать @JoinTable, как в следующем примере:
Встроенная ссылка (embedded reference) позволяет часть данных выделить в отдельный класс, при этом сохраняя связывание этих данных в ту же физичекую таблицу БД, что и остальные данные основной сущности. Это удобно, когда мы имеем сложные вспомогательные данные, которые мы используем в нескольких сущностях, и не хотим использовать или не можем использовать наследование. Это удобно для вспомогателтельных классов, которые мы используем в нескольких приложениях. Хорошим примером такого подхода является класс Address, содержащий поля адреса. Он может использоваться не только для класса Customer, но и других классов и не только в этом приложении. Такое отделение позволяет облегчить манипулирование адресом как монолитным набором данных. В нашем примере рассмотрена сущность Customer, в которой реализуется встроенная ссылка на Address. Мы используем возможности JPA для реализации этого.
Использование аннотации @AttributeOverrides позволяет нам связать поля таблицы БД корректным образом:
Если не использовать @AttributeOverrides, будут применены значения по-умолчанию.
Преобразование типов данных(Type conversion)
Преобразование типов данных от Java к реляционной базе данных и обратно является заботой применяемой реализации JPA (OpenXava использует Hibernate по-умолчанию). Обычно стандартное преобразование типов вполне приемлемо для болшинства случаев, особенно. Тем не менее, иногда, особенно, если вы работаете с уже существующей базой данных, возникает потребность в нестандартном преобразовании данных. Об этом мы расскажем ниже.
В OpenXava мы используем возможности приведения типов, предоставляемые Hibernate. Более подробную информацию можно получить в документации Hibernate .
Преобразование типа свойства (Property conversion)
Если тип данных переменной Java (Java property) и соответствующего столбца в таблице БД не совпадают, то нам необходимо создать Hibernate Type для реализации логики преобразования типов.
Например, если мы в Java хотим работать с типом String [], при этом при передаче в БД мы хотим соединять массив строк в одну строку типа VARCHAR. В этом случае нужно использовать аннотацию @Type для задания преобразования типов следующим образом:
Класс для преобразования типов должен реализовывать интерфейс org.hibernate.usertype.UserType (1). Основные методы, которые нужно реализовать, - это nullSafeGet (2), предназначенный для чтения из БД преобразования в тип Java, и nullSafeSet (3) для обратного преобразования.
OpenXava реализует набор готовых преобразователей типов в пакете org.openxava.types . Например, EnumLetterType, который позволяет связать свойство с типом enum. Давайте рассмотрим на примере реализацию свойства с использованием enum:
private Distance distance;publicenum Distance { LOCAL, NATIONAL, INTERNATIONAL };
Стандартное поведение системы таково, что при записи в БД значение 'LOCAL' будет сохранено как 1, 'NATIONAL' как 2 и 'INTERNATIONAL' как 3. Давайте предположим, что нам необходимо, чтобы в БД должны вместо числовых значений сохранятья буквы ('L', 'N' or 'I'). Чтобы реализовать такое сохранение в БД, мы можем использовать тип EnumLetterType:
Так как мы указали 'LNI' в качестве значения letters, то преобразователь типов будет предполагать, что 'L' - это 1, 'N' - 2 и 'I' - это 3. Как вы видите из примера, преобразователи типов конфигуруются с помощью своих свойств. Что позволяет их использовать довольно широко.
Связывание одного свойства с несколькими столбцами (Multiple column conversion)
Используя CompositeUserType, мы можем привязать несколько столбцов таблицы БД к одному свойству в Java. Это пригодиться, например. когда ваше свойство в свою очередь является классом, при сохранении которого используется несколько аттрибутов. Так же такое связывание используется, когда мы работаем с базами данных с уже предопределенной структурой (не нами:-)).
Пример класса-конвертора Date3Type. Он реализует преобразование между тремя столбцами в БД и одним свойством типа java.util.Date в Java.
Как мы говорили выше, класс-конвертор должен реализовывать интерфейс CompositeUserType (1). Ключевыми методами данного класса являются методы getPropertyValue (2) и setPropertyValue (3), которые соответственно применяются для получения (get) и установки (set) значений свойств in the properties of the object of the composite type, and nullSafeGet (4) and nullSafeSet (5) for reading and storing this object from and to database.
Преобразования типа для Ссылки (Reference conversion)
Преобразование типа данных для Ссылки (Reference) не поддерживается напрямую в Hibernate. Иногда в крайне редких обстоятельствах вам все же, возможно, понадобится реализовать таоке пhеобразование. Сейчас мы расскажем, как это сделать.
К примеру, вы используете ссылку на объект "Водительские права" (driver licence), используя два столбца в таблице БД DRIVINGLICENCE_LEVEL и DRIVINGLICENCE_TYPE. При этом столбец DRIVINGLICENCE_TYPE не может иметь значения null, но при том же возможно, что объект не имеет ссылки на driving incense. В таком случае столбец DRIVINGLICENCE_TYPE содержит пустую стоку. Такой дизайн не является хорошей практикой, не используйте его, если сами проектируете БД, используете foreign keys. Но в реальной жизни такое случается, если разработчик БД не умеет обращаться с значениями null.
В таком случае вам необходимо подменять null на пустую строку при работе с DRIVINGLICENCE_TYPE. Это может быть достигнуто следующим образом:
// We apply conversion (null into an empty String) to DRIVINGLICENCE_TYPE column// In order to do it, we create drivingLicence_level and drivingLicence_type// We make JoinColumns not insertable nor updatable, we modify the get/setDrivingLincence methods// and we create a drivingLicenceConversion() method.
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({// 1
@JoinColumn(name="DRIVINGLICENCE_LEVEL", referencedColumnName="LEVEL",
insertable=false, updatable=false),
@JoinColumn(name="DRIVINGLICENCE_TYPE", referencedColumnName="TYPE",
insertable=false, updatable=false)})private DrivingLicence drivingLicence;privateInteger drivingLicence_level;// 2privateString drivingLicence_type;// 2public DrivingLicence getDrivingLicence(){// 3// In this way because the column for type of driving lincence does not admit nulltry{if(drivingLicence != null) drivingLicence.toString();// to force loadreturn drivingLicence;}catch(EntityNotFoundException ex){returnnull;}}publicvoid setDrivingLicence(DrivingLicence licence){// 4// In this way because the column for type of driving lincence does not admit nullthis.drivingLicence = licence;this.drivingLicence_level = licence==null?null:licence.getLevel();this.drivingLicence_type = licence==null?null:licence.getType();}
@PrePersist @PreUpdate
privatevoid drivingLicenceConversion(){// 5if(this.drivingLicence_type == null)this.drivingLicence_type = "";}
Во-первых, необходимо использовать аннтотацию @JoinColumns с параметрами insertable=false и updatable=false для всех включенных аннотаций @JoinColumn (1). Как результат свойство-ссылка будет загружаться из БД, но не будет записываться обратно в БД. Также необходимо определить обычное свойсвто для хранения первичного ключа ссылки (2).
Теперь нам нужно написать метод получения значения (getter) getDrivingLicence() (3), который будет возвращать null, если ссылка не найдена, и метод установки значения (setter) setDrivingLicence() (4) для присвоения значения ключа для ссылки в соответствующие обычне свойства.
И наконец нам нужно запрограммировать метод обратного вызоваdrivingLincenceConversion() (5), чтобы преобразование типов заработало. Данный метод будет автоматически вызываться при операциях создания и изменения (create и update).
Вышеприведенный пример показывает, как можно работать адаптировать OX к работе с уже существующей структурой данных в БД, приложив небольшие усилия в программировании и использовав базовые возможности JPA.
Table of Contents
Глава 6: Объектно-реляционное связывание
Объектно-реляционное связывание (Object-relational mapping, ORM) реализует привязку объектов Java к БД, позволяя описывать какие таблицы и столбцы БД будут использованы для хранения постоянного состояния объектов.Системы объектно-реляционного связывания (Object/relational tools) позволяют разработчику сконцентироваться на работе с объектами Java. Они обеспечивают автоматическое связывание объектов с БД, включая автоматическую генерацию кода SQL для выборки и модификации данных в БД. При использовании таких средств нет необходимости написании кода для прямого взаимодействия с БД. Но привязку ваших объектов Java к БД все таки описать нужно.
OpenXava использует Java Persistence API (JPA) для реализации ORM. В данном главе мы расскажем о базовых техниках применения JPA и рассмотрим несколько распространнёных случаев, вызывающих затруднение у начинающих работать с JPA. Дополнительную информацию о JPA можно почерпнуть из документации по Hibernate Annotations (реализация JPA, используемая OpenXava по-умолчанию), также вы можете использовать любую другую документацию по JPA.
Связывание объекта (Entity mapping)
Аннотация @Table (Таблица) указывает основную таблицу для связываемого класса сущности (entity). С помощью аннонтации @SecondaryTable (если речь об одной дополнительной таблице) или @SecondaryTables (если нужно указать несколько таблиц) можно указать дополнительные таблицы.Пример определения сущности:
Аттрибут name задает имя таблицы, если оно не соответствует соглашению по наименованию JPA.
Аттрибут schema задает схему, в которой будет размещена таблица.
Аттрибуты не обязательны, полный список аттрибутов смотрите в документации JPA.
Если для сущности не указано определение аннонтации @Table, предполагается, что имя таблицы соответствует соглашениям по наименованию, принятым в JPA.
Связывание свойства (Property mapping)
Аннотация @Column (Столбец) используется для указания столбца таблицы, с которым связывается переменная-член класса сущности. Если для свойства аннотация @Column не определена, то при связывании используются значения и соглашения, принятые в JPA по-умолчанию.Простейший пример описания свойства:
В примере переопределяется имя столбца аттрибутом name и задается длина данных аттрибутом length.
Пример с аннотированием метода установки значения свойства (getter):
Еще примеры:
Связывание Ссылки (Reference mapping)
Аннотация @JoinColumn (Стоблец, используемый для соединения) используется для определения свойства, которое ссылается на другую сущность.ПЕР: В примере ниже наша сущность имеет ссылку на Invoice (Счет).
Пример:
Для связывания с композитным внешним ключом (composite foreign keys) используйте @JoinColumns. Это аннотация позволяет группировать вместе аннотации @JoinColumn, относящиеся к одной ссылке.
При использовании @JoinColumns аттрибуты name и referencedColumnName должны быть определены для каждой аннотации @JoinColumn.
Пример:
Связывание Коллекции (Collection mapping)
При использовании @OneToMany для коллекций обычно нет необходимости делать что-либо дополнительное на второй стороне ассоциации. Но если вы используете @ManyToMany, то возможно будет полезным использовать @JoinTable, как в следующем примере:Если @JoinTable не указан, то применяются значения по-умолчанию.
Связывание Встроенных ссылок (Embedded reference mapping)
Встроенная ссылка (embedded reference) позволяет часть данных выделить в отдельный класс, при этом сохраняя связывание этих данных в ту же физичекую таблицу БД, что и остальные данные основной сущности. Это удобно, когда мы имеем сложные вспомогательные данные, которые мы используем в нескольких сущностях, и не хотим использовать или не можем использовать наследование. Это удобно для вспомогателтельных классов, которые мы используем в нескольких приложениях. Хорошим примером такого подхода является класс Address, содержащий поля адреса. Он может использоваться не только для класса Customer, но и других классов и не только в этом приложении. Такое отделение позволяет облегчить манипулирование адресом как монолитным набором данных. В нашем примере рассмотрена сущность Customer, в которой реализуется встроенная ссылка на Address. Мы используем возможности JPA для реализации этого.Использование аннотации @AttributeOverrides позволяет нам связать поля таблицы БД корректным образом:
Если не использовать @AttributeOverrides, будут применены значения по-умолчанию.
Преобразование типов данных(Type conversion)
Преобразование типов данных от Java к реляционной базе данных и обратно является заботой применяемой реализации JPA (OpenXava использует Hibernate по-умолчанию). Обычно стандартное преобразование типов вполне приемлемо для болшинства случаев, особенно. Тем не менее, иногда, особенно, если вы работаете с уже существующей базой данных, возникает потребность в нестандартном преобразовании данных. Об этом мы расскажем ниже.В OpenXava мы используем возможности приведения типов, предоставляемые Hibernate. Более подробную информацию можно получить в документации Hibernate .
Преобразование типа свойства (Property conversion)
Если тип данных переменной Java (Java property) и соответствующего столбца в таблице БД не совпадают, то нам необходимо создать Hibernate Type для реализации логики преобразования типов.Например, если мы в Java хотим работать с типом String [], при этом при передаче в БД мы хотим соединять массив строк в одну строку типа VARCHAR. В этом случае нужно использовать аннотацию @Type для задания преобразования типов следующим образом:
Логика преобразования типов определена в классе RegionsType:
Класс для преобразования типов должен реализовывать интерфейс org.hibernate.usertype.UserType (1). Основные методы, которые нужно реализовать, - это nullSafeGet (2), предназначенный для чтения из БД преобразования в тип Java, и nullSafeSet (3) для обратного преобразования.
OpenXava реализует набор готовых преобразователей типов в пакете org.openxava.types . Например, EnumLetterType, который позволяет связать свойство с типом enum. Давайте рассмотрим на примере реализацию свойства с использованием enum:
Стандартное поведение системы таково, что при записи в БД значение 'LOCAL' будет сохранено как 1, 'NATIONAL' как 2 и 'INTERNATIONAL' как 3. Давайте предположим, что нам необходимо, чтобы в БД должны вместо числовых значений сохранятья буквы ('L', 'N' or 'I'). Чтобы реализовать такое сохранение в БД, мы можем использовать тип EnumLetterType:
Так как мы указали 'LNI' в качестве значения letters, то преобразователь типов будет предполагать, что 'L' - это 1, 'N' - 2 и 'I' - это 3. Как вы видите из примера, преобразователи типов конфигуруются с помощью своих свойств. Что позволяет их использовать довольно широко.
Связывание одного свойства с несколькими столбцами (Multiple column conversion)
Используя CompositeUserType, мы можем привязать несколько столбцов таблицы БД к одному свойству в Java. Это пригодиться, например. когда ваше свойство в свою очередь является классом, при сохранении которого используется несколько аттрибутов. Так же такое связывание используется, когда мы работаем с базами данных с уже предопределенной структурой (не нами:-)).Пример класса-конвертора Date3Type. Он реализует преобразование между тремя столбцами в БД и одним свойством типа java.util.Date в Java.
DAYDELIVERY, MONTHDELIVERY и YEARDELIVERY - это 3 стобца в БД, в которых хранится дата доставки. И наконец давайте взглянем на Date3Type:
Как мы говорили выше, класс-конвертор должен реализовывать интерфейс CompositeUserType (1). Ключевыми методами данного класса являются методы getPropertyValue (2) и setPropertyValue (3), которые соответственно применяются для получения (get) и установки (set) значений свойств in the properties of the object of the composite type, and nullSafeGet (4) and nullSafeSet (5) for reading and storing this object from and to database.
Преобразования типа для Ссылки (Reference conversion)
Преобразование типа данных для Ссылки (Reference) не поддерживается напрямую в Hibernate. Иногда в крайне редких обстоятельствах вам все же, возможно, понадобится реализовать таоке пhеобразование. Сейчас мы расскажем, как это сделать.К примеру, вы используете ссылку на объект "Водительские права" (driver licence), используя два столбца в таблице БД DRIVINGLICENCE_LEVEL и DRIVINGLICENCE_TYPE. При этом столбец DRIVINGLICENCE_TYPE не может иметь значения null, но при том же возможно, что объект не имеет ссылки на driving incense. В таком случае столбец DRIVINGLICENCE_TYPE содержит пустую стоку. Такой дизайн не является хорошей практикой, не используйте его, если сами проектируете БД, используете foreign keys. Но в реальной жизни такое случается, если разработчик БД не умеет обращаться с значениями null.
В таком случае вам необходимо подменять null на пустую строку при работе с DRIVINGLICENCE_TYPE. Это может быть достигнуто следующим образом:
Во-первых, необходимо использовать аннтотацию @JoinColumns с параметрами insertable=false и updatable=false для всех включенных аннотаций @JoinColumn (1). Как результат свойство-ссылка будет загружаться из БД, но не будет записываться обратно в БД. Также необходимо определить обычное свойсвто для хранения первичного ключа ссылки (2).
Теперь нам нужно написать метод получения значения (getter) getDrivingLicence() (3), который будет возвращать null, если ссылка не найдена, и метод установки значения (setter) setDrivingLicence() (4) для присвоения значения ключа для ссылки в соответствующие обычне свойства.
И наконец нам нужно запрограммировать метод обратного вызова drivingLincenceConversion() (5), чтобы преобразование типов заработало. Данный метод будет автоматически вызываться при операциях создания и изменения (create и update).
Вышеприведенный пример показывает, как можно работать адаптировать OX к работе с уже существующей структурой данных в БД, приложив небольшие усилия в программировании и использовав базовые возможности JPA.